/**
 * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information regarding copyright ownership. Apereo
 * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use
 * this file except in compliance with the License. You may obtain a copy of the License at the
 * following location:
 *
 * <p>http://www.apache.org/licenses/LICENSE-2.0
 *
 * <p>Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apereo.portal.tools.dbloader;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardOpenOption.APPEND;
import static java.nio.file.StandardOpenOption.CREATE;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.sql.Connection;
import java.sql.SQLException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullWriter;
import org.apache.commons.lang.StringUtils;
import org.apereo.portal.hibernate.DelegatingHibernateIntegrator.HibernateConfiguration;
import org.apereo.portal.hibernate.HibernateConfigurationAware;
import org.hibernate.cfg.Configuration;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.jdbc.internal.FormatStyle;
import org.hibernate.engine.jdbc.internal.Formatter;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.tool.hbm2ddl.FixedDatabaseMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.jdbc.UncategorizedSQLException;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcOperations;

/** Runs the Hibernate Schema Export tool using the specified DataSource for the target DB. */
public class DataSourceSchemaExport implements ISchemaExport, HibernateConfigurationAware {
    protected final Logger logger = LoggerFactory.getLogger(getClass());

    private final Formatter formatter = FormatStyle.DDL.getFormatter();
    private JdbcOperations jdbcOperations;
    private Configuration configuration;
    private Dialect dialect;
    private String persistenceUnit;

    /** The name of the persistence unit to use */
    @Required
    public void setPersistenceUnit(String persistenceUnit) {
        this.persistenceUnit = persistenceUnit;
    }

    @Required
    public void setJdbcOperations(JdbcOperations jdbcOperations) {
        this.jdbcOperations = jdbcOperations;
    }

    @Override
    public boolean supports(String persistenceUnit) {
        return this.persistenceUnit.equals(persistenceUnit);
    }

    @Override
    public String getPersistenceUnitName() {
        return this.persistenceUnit;
    }

    @Override
    public void setConfiguration(
            String persistenceUnit, HibernateConfiguration hibernateConfiguration) {
        this.configuration = hibernateConfiguration.getConfiguration();
        final SessionFactoryImplementor sessionFactory = hibernateConfiguration.getSessionFactory();
        this.dialect = sessionFactory.getDialect();
    }

    @Override
    public void drop(boolean export, String outputFile, boolean append) {
        final String[] dropSQL = configuration.generateDropSchemaScript(dialect);
        perform(dropSQL, export, outputFile, append, false);
    }

    @Override
    public void create(boolean export, String outputFile, boolean append) {
        final String[] createSQL = configuration.generateSchemaCreationScript(dialect);
        perform(createSQL, export, outputFile, append, true);
    }

    @Override
    public void update(boolean export, String outputFile, boolean append) {
        final String[] updateSQL =
                this.jdbcOperations.execute(
                        new ConnectionCallback<String[]>() {
                            @Override
                            public String[] doInConnection(Connection con)
                                    throws SQLException, DataAccessException {
                                final FixedDatabaseMetadata databaseMetadata =
                                        new FixedDatabaseMetadata(con, dialect);
                                return configuration.generateSchemaUpdateScript(
                                        dialect, databaseMetadata);
                            }
                        });

        perform(updateSQL, export, outputFile, append, true);
    }

    private void perform(
            String[] sqlCommands,
            boolean executeSql,
            String outputFile,
            boolean append,
            boolean failFast) {
        final PrintWriter sqlWriter = getSqlWriter(outputFile, append);
        try {
            for (final String sqlCommand : sqlCommands) {
                final String formatted = formatter.format(sqlCommand);
                sqlWriter.println(formatted);

                if (executeSql) {
                    try {
                        jdbcOperations.execute(sqlCommand);
                        logger.info(sqlCommand);
                    } catch (BadSqlGrammarException | UncategorizedSQLException e) {
                        // For HSQL database and ant db-update to avoid failing when attempting to
                        // delete a sequence that does not exist.
                        // Needed until Hibernate 5.  See
                        // https://hibernate.atlassian.net/browse/HHH-7002.
                        if (sqlCommand.contains("drop constraint")) {
                            logger.info(
                                    "Failed to execute (probably ignorable): {}, error message {}",
                                    sqlCommand,
                                    e.getMessage());
                        } else {
                            handleSqlException(failFast, sqlCommand, e);
                        }
                    } catch (Exception e) {
                        handleSqlException(failFast, sqlCommand, e);
                    }
                }
            }
        } finally {
            IOUtils.closeQuietly(sqlWriter);
        }
    }

    private void handleSqlException(boolean failFast, String sqlCommand, Exception e) {
        if (failFast) {
            logger.error("Failed to execute: {}\n\t{}", sqlCommand, e.getMessage());
            throw new RuntimeException("Failed to execute: " + sqlCommand, e);
        } else {
            logger.info(
                    "Failed to execute (probably ignorable): {}, error message {}",
                    sqlCommand,
                    e.getMessage());
        }
    }

    private PrintWriter getSqlWriter(String outputFile, boolean append) {
        final Writer sqlWriter;
        if (StringUtils.trimToNull(outputFile) != null) {
            try {
                // Insure any parent directories are created so we don't fail creating the SQL file.
                File file = new File(outputFile);
                if (!file.exists()) {
                    file.getParentFile().mkdirs();
                }
                sqlWriter =
                        Files.newBufferedWriter(
                                file.toPath(),
                                UTF_8,
                                append
                                        ? new StandardOpenOption[] {CREATE, APPEND}
                                        : new StandardOpenOption[] {CREATE});
            } catch (IOException e) {
                throw new RuntimeException("", e);
            }
        } else {
            sqlWriter = new NullWriter();
        }
        return new PrintWriter(sqlWriter);
    }
}
